package cn.zhxu.toys.captcha.impl;

import cn.zhxu.toys.captcha.CodeGenerator;

import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 运算式生成器
 * Created by junxiang on 2019/8/2
 */
public class RandomFormulaCodeGenerator implements CodeGenerator {

    private static final char ADDITION = '+';
    private static final char SUBTRACTION = '-';
    private static final char MULTIPLY = '*';
    private static final char DIVISION = '÷';
    private static final char[] OPERATORS = new char[] { ADDITION, SUBTRACTION, MULTIPLY, DIVISION };
    private static final char EQUAL_SIGN = '=';

    private static final Random random = ThreadLocalRandom.current();

    /**
     * 长度  公式
     *  1 ： 1
     *  2 ： 1 =
     *  3 ： 1 + 2
     *  4 ： 1 + 2 =
     *  5 ： 1 + 2 * 3
     *  6 ： 1 + 2 * 3 =
     */
    private int length = 6;

    /**
     * 是否允许结果为负数
     */
    private boolean allowNegative = false;

    public RandomFormulaCodeGenerator() {
    }

    public RandomFormulaCodeGenerator(int length) {
        this(length, false);
    }

    public RandomFormulaCodeGenerator(int length, boolean allowNegative) {
        setLength(length);
        this.allowNegative = allowNegative;
    }

    @Override
    public CodeResult generate() {
        int[] nums = generateNumbers();
        char[] ops = generateOperators(nums);
        int result = compute(ops, nums);
        while (result == Integer.MAX_VALUE || (!allowNegative && result < 0)) {
            ops = generateOperators(nums);
            result = compute(ops, nums);
        }
        String code = buildCode(ops, nums);
        String check = String.valueOf(result);
        return new CodeResult(code, check);
    }

    private String buildCode(char[] ops, int[] nums) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < ops.length; i++) {
            sb.append(nums[i]).append(ops[i]);
        }
        sb.append(nums[nums.length - 1]);
        if (length % 2 == 0) {
            sb.append(EQUAL_SIGN);
        }
        return sb.toString();
    }

    private int[] generateNumbers() {
        int[] nums = new int[(length + 1) / 2];
        for (int i = 0; i < nums.length; i++) {
            nums[i] = random.nextInt(10);
        }
        return nums;
    }

    private char[] generateOperators(int[] nums) {
        char[] ops = new char[nums.length - 1];
        for (int i = 0; i < ops.length; i++) {
            ops[i] = OPERATORS[random.nextInt(OPERATORS.length)];
        }
        return ops;
    }

    /**
     * 获取运算结果
     * @param ops 运算符
     * @param nums 操作数
     * @return 运算式结果，Integer.MAX_VALUE 表示不可计算
     */
    private int compute(char[] ops, int[] nums) {
        if (ops.length != nums.length - 1) {
            throw new IllegalArgumentException("Invalid ops or nums");
        }
        if (nums.length == 1) {
            return nums[0];
        }
        if (nums.length == 2) {
            return compute(ops[0], nums[0], nums[1]);
        }
        if (ops[0] == MULTIPLY || ops[0] == DIVISION || ops[1] == ADDITION || ops[1] == SUBTRACTION) {
            return compute(ops[1], compute(ops[0], nums[0], nums[1]), nums[2]);
        }
        return compute(ops[0], nums[0], compute(ops[1], nums[1], nums[2]));
    }

    /**
     *  获取运算结果
     * @param op 运算符
     * @param n1 运算式第一个值
     * @param n2 运算式第二个值
     * @return 运算式结果，Integer.MAX_VALUE 表示不可计算
     */
    private int compute(char op, int n1, int n2) {
        if (n1 == Integer.MAX_VALUE || n2 == Integer.MAX_VALUE) {
            return Integer.MAX_VALUE;
        }
        switch (op) {
            case ADDITION:
                return n1 + n2;
            case SUBTRACTION:
                return n1 - n2;
            case MULTIPLY:
                return n1 * n2;
            case DIVISION:
                if (n2 == 0 || n1 % n2 != 0) {
                    return Integer.MAX_VALUE;
                }
                return n1 / n2;
        }
        throw new IllegalArgumentException("Invalid Op: " + op);
    }

    public void setLength(int length) {
        this.length = Math.min(Math.max(1, length), 6);
    }

    public int getLength() {
        return length;
    }

    public boolean isAllowNegative() {
        return allowNegative;
    }

    public void setAllowNegative(boolean allowNegative) {
        this.allowNegative = allowNegative;
    }

}
